package jers;

import java.io.File;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.rmi.activation.ActivationSystem;
import java.rmi.registry.Registry;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

import jers.impl.SystemFactory;
import jers.util.Configurator;
import jers.util.FileClassLoader;
import jers.util.Logger;
import jers.util.SimpleConfigurator;
import jers.util.Utility;


/**
 * <h1>JERS - Framework Java para Execucao Remota de Servicos.</h1>
 * <p>
 * Classe que inicializa o runtime do framework inicializando o provedor 
 * de servicos remotos e disponibiliza em sua interface os metodos necessarios
 * para a utilizacao do framework tanto no cliente quanto no servidor.
 * <br>
 * </p>
 * 
 * @author 
 * 		<a href="mailto:felipe.zappala@gmail.com">
 * 			Luis Felipe Zappala
 * 		</a> [ <a href="http://lncc.br/ist">ISTCC-P</a> / 2009 ]
 */
public final class JERS {
	
	/**
	 * Construtor privado para evitar instanciacao`
	 */
	private JERS() {
		super();
	}

	//
	// Commands constants
	//
	private static final String SERVER = "--server";
	private static final String CLIENT = "--client";
	private static final String LIBS   = "--libs";
	private static final String INFO   = "--info";
	private static final String HELP   = "--help";
	
	/**
	 * Finaliza a JVM em execucao com codigo de sucesso.
	 */
	public static void systemExit() {
		systemExit(0);
	}
	
	/**
	 * Finaliza a JVM em execucao com codigo passado.
	 */
	public static void systemExit(int status) {
		System.exit(status);
	}
	
	/**
	 * Imprime informacoes do framework na saida passada.
	 */
	private static void info(PrintStream out) {
		out.println(Utility.fill('-', 80));
		out.println(Utility.concat("[JERS] INFORMATION: \n",
				"Framework Java para Execucao Remota de Servicos \n",
				"Author: Luis Felipe Zappala [ISTCC-P / 2009]"
		));
		out.println(Utility.fill('-', 80));
	}
	
	/**
	 * Inprime a ajuda do framework na saida passada. 
	 */
	private static void help(PrintStream out) {
		out.println(Utility.fill('-', 80));
		out.println(Utility.concat("[JERS] HELP: \n",
				"use '",SERVER,"' to start the framework in the server mode.\n",
				"To exit this mode press 'ctrl + c' \n\n",
				"use '",CLIENT,"' to start the framework in the client mode.\n",
				"This mode requires the full name of the client class \n",
				"and its parameters, if exists."
		));
		out.println(Utility.fill('-', 80));
	}
	
	/**
	 * Inprime o exemplo de uso do framework na saida passada. 
	 */
	private static void example(PrintStream out) {
		char ps = File.pathSeparatorChar;
		
		out.println(Utility.fill('-', 80));
		out.println(Utility.concat("[JERS] EXAMPLE: \n",
				"java -jar jers.jar ",SERVER,"\n\n",
				"java -jar jers.jar ",LIBS," .", ps, "dep1.jar", ps, "dep2.jar",
				"\n\t ", CLIENT, " package.ClientClassName",
				"param1 param2 paramN"
		));
		out.println(Utility.fill('-', 80));
	}
	
	/**
	 * Obtem a fabrica de instancias do sistema {@link Factory}.
	 * <br>
	 * Retorna a instancia da fabrica configurada sob a propriedade
	 * {@code jers.service.classname.factory}. A implementacao padrao
	 * se encontra na classe {@link SystemFactory}.
	 */
	public static Factory createFactory() {
		Factory factory;
		try {
			Configurator conf = new SimpleConfigurator();
			String factoryClass = conf.getProperty(
					"jers.service.classname.factory");
			factory = (Factory) Class.forName(factoryClass).newInstance();
			
		} catch (Exception e) {
			System.err.println(Utility.concat(
					"[JERS][ERROR] Error on initialize the factory class: ",
					e.getClass().getName(), " -> ", e.getMessage(), 
					(e.getCause() != null) 
						? Utility.concat(" - Caused by: ", 
								e.getCause().getClass().getName(), " -> ",
								e.getCause().getMessage())
						: "" 
			));
			factory = new SystemFactory();
		}
		return factory;
	}
	
	/**
	 * Atributo da fabrica {@link Factory} padrao do sistema.
	 */
	public static final Factory factory = createFactory();
	
	/**
	 * Atributo da configuracao {@link Configurator} padrao do sistema.
	 */
	public static final Configurator config = factory.newConfigurator();
	
	/**
	 * Atributo do logger {@link Logger} padrao do sistema.
	 */
	public static final Logger log = factory.newLogger();
	
	/**
	 * Obtem uma instancia proxy remoto para um objeto {@link Service}.
	 * 
	 * @param
	 * 		server - Endereco IP e porta do servidor de servicos.<br /> 
	 * 				 Formato esperado {@code <IP:PORT>} ou 
	 * 				 {@code <DOMINIO:PORT>}
	 * @return
	 * 		Uma implementacao proxy remoto para o objeto de servico.
	 * @throws 
	 * 		RemoteException caso ocorra algum erro remoto.
	 * @see
	 * 		{@link Manager}
	 */
	public static Service getService(String server) 
			throws RemoteException {
		return factory.newManager().getService(server);
	}
	
	/** 
	 * O numero da porto padrao do servico. Se nao configurado, 
	 * usar {@link Registry#REGISTRY_PORT} [1099] por padrao. 
	 */
	public static final int SERVICE_PORT;
	
	static {
		int servicePort = Registry.REGISTRY_PORT;
		try {
			String port = config.getProperty("jers.service.port");
			servicePort = Integer.parseInt(port);
		} catch (Exception e) {
			// Do nothing
		}
		SERVICE_PORT = servicePort;
	}
	
	/** 
	 * O numero da porta padrao do servico ativavel. Se nao configurado, 
	 * usar {@link ActivationSystem#SYSTEM_PORT} [1098] por padrao. 
	 */
	public static final int ACTIVATION_PORT;
	
	static {
		int activationPort = ActivationSystem.SYSTEM_PORT;
		try {
			String port = config.getProperty("jers.service.activation.port");
			activationPort = Integer.parseInt(port);
		} catch (Exception e) {
			// Do nothing
		}
		ACTIVATION_PORT = activationPort;
	}

	/**
	 * O nome do servico que vai ser registrado no RMI Registry.
	 * Caso nao configurado o padrao sera {@code jers.Service}.
	 */
	public static final String SERVICE_NAME;
	
	static {
		String name = JERS.config.getProperty("jers.service.name");
		if (Utility.validate(name)) {
			SERVICE_NAME = name;			
		} else {
			SERVICE_NAME = "jers.Service";
		}
	}
	
	/**
	 * O nome do servico de callback que vai ser registrado no RMI Registry. 
	 * Caso nao configurado o padrao sera {@code jers.Callback}.
	 */
	public static final String CALLBACK_NAME;
	
	static {
		String name = JERS.config.getProperty("jers.callback.name");
		if (Utility.validate(name)) {
			CALLBACK_NAME = name;			
		} else {
			CALLBACK_NAME = "jers.Callback";
		}
	}
	
	/** 
	 * O numero da porta padrao do servico de callback. Se nao configurado, 
	 * usar {@link Registry#REGISTRY_PORT} [1099] por padrao. 
	 */
	public static final int CALLBACK_PORT;
	
	static {
		int callbackPort = Registry.REGISTRY_PORT;
		try {
			String port = config.getProperty("jers.callaback.port");
			callbackPort = Integer.parseInt(port);
		} catch (Exception e) {
			// Do nothing
		}
		CALLBACK_PORT = callbackPort;
	}
	
	/**
	 * Inicializa o JERS em modo servidor.
	 * 
	 * @throws 
	 * 		Exception - caso ocorra algum erro.
	 */
	public static synchronized void startServer() {
		JERS.log.info("Start server mode");
		
		if (provider == null) {
			provider = factory.newProvider();
		}
		provider.start();
	}
	
	/**
	 * Finaliza o JERS que esteja executando em modo servidor.
	 * 
	 * @throws 
	 * 		Exception - caso ocorra algum erro.
	 */
	public static void stopServer() {
		JERS.log.info("Stop server mode");
		provider.stop();
	}
	
	/**
	 * Inicializa o JERS em modo cliente.
	 * 
	 * @param 
	 * 		clientClass - Classe que contem o metodo main a ser executado.
	 * @param 
	 * 		parameters - Parametros a serem passados ao metodo main.
	 * @throws 
	 * 		Exception - caso ocorra algum erro.
	 */
	public static void startClient(Class<?> clientClass, String... parameters) {
		JERS.log.info("Start client mode");
		try {
			Method method = clientClass.getMethod("main", String[].class);
			method.setAccessible(true); // Forca o acesso ao metodo.
			method.invoke(clientClass, (Object) parameters);
		} catch (Exception e) {
			throw new RuntimeException("Error on start client mode", e);
		}
	}
	
	/**
	 * Metodo para inicializacao do framework em linha de comando.
	 * Executa o provedor de tarefas por linha de comando. 
	 * Utilize o comando '--help' para obter informacoes de uso. 
	 * 
	 * @param 
	 * 		args - Os argumentos de inicializacao.
	 * @throws Exception 
	 */
	public static void main(String[] args) throws Exception {
		Thread main = Thread.currentThread();
		
		switch (executionMode(args)) {
		case SERVER:
			main.setName(Utility.concat("JERS-Server-", main.getName()));
			startServer();
			exit(0);
			break;

		case CLIENT:
			main.setName(Utility.concat("JERS-Client-", main.getName()));
			loadClient(clientFile(args), clientParams(args),  clientLibs(args));
			exit(0);
			break;
			
		case HELP:
			showHelpInfo();
			break;
			
		case NONE:
			exit(1);
			break;
		}
	}
	
	/**
	 * Retorna os parametros para a classe cliente a ser executada 
	 */
	private static String[] clientParams(String[] args) {
		if (CLIENT.equals(args[0])) {
			return parametersCopy(2, args);	
		} 
		return parametersCopy(4, args);
	}

	/**
	 * Separa uma string no separador de caminhos de arquivo do sistema
	 */
	private static String[] clientLibs(String[] args) {
		if (LIBS.equals(args[0])) {
			String[] splited = args[1].split("\\" + File.pathSeparator);
			List<String> paths = new ArrayList<String>(splited.length + 1);
			
			for (String s : splited) {
				paths.add(s.trim());
			}
			if (!paths.contains(".")) {
				paths.add("."); // Para incluir o diretorio atual por padrao	
			}
			return paths.toArray(new String[paths.size()]);	
		}
		return null;
	}
	
	/**
	 * Separa uma string no separador de caminhos de arquivo do sistema
	 */
	private static String clientFile(String[] args) {
		if (CLIENT.equals(args[0])) {
			return args[1];	
		}
		return args[3];
	}

	/**
	 * Copia o array de parametros passado para um novo. 
	 */
	private static String[] parametersCopy(int pos, String[] args) {
		String[] copy = new String[args.length -2];
		System.arraycopy(args, pos, copy, 0, args.length-pos);
		return copy;
	}

	/**
	 * Provedor do framework em modo server. 
	 */
	private static Provider provider;
	

	/**
	 * Carrega o framework em modo cliente.
	 */
	private static void loadClient(String className, String[] args, 
			String[] libs) {
		try {
			String[] paths = libs; 
			if (paths == null) {
				// Adiciona o path corrente de execucao por padrao
				paths = new String[]{"."}; 
			}
			ClassLoader loader = new FileClassLoader(Utility.toFiles(paths));
			Class<?> clazz = Class.forName(className, true, loader);
			startClient(clazz, args);
			
		} catch (Exception e) {
			JERS.log.error(e, "Load client error: ", e.getMessage());
			exit(1);
		}
	}
	
	/**
	 * Define os modos de execucao do framework.
	 */
	private enum ExecutionMode {
		NONE, SERVER, CLIENT, HELP;
	}
	
	/**
	 * Obtem o mode de execucao de acordo com os parametros passados na main.
	 */
	private static ExecutionMode executionMode(String[] args) {
		ExecutionMode execMode = ExecutionMode.NONE;
		
		if (args != null && args.length > 0) {
			if (SERVER.equals(args[0])) {
				execMode = ExecutionMode.SERVER;
				
			} else if (CLIENT.equals(args[0]) && args.length >= 2) {
				execMode = ExecutionMode.CLIENT;
				
			} else if (LIBS.equals(args[0]) && args.length >= 4) {
				execMode = ExecutionMode.CLIENT;
				
			} else if(HELP.equals(args[0]) || INFO.equals(args[0])) {
				execMode = ExecutionMode.HELP;
			}
		}
		return execMode;
	}
	
	/** 
	 * Mostra na saida padrao as informacoes de ajuda do sistema.
	 */
	private static void showHelpInfo() {
		help(System.out);
		example(System.out);
		info(System.out);
	}
	
	/**
	 * Controla a saida do servidor por linha de comando.
	 */
	private static final void exit(int status) {
		if (status > 0) {
			String exit = Utility.concat("[JERS] There was an error, ",
					"verify the correct use of the command using the ",
					"parameter --help");
			System.err.println(exit);
			System.exit(status);
		}
		lockTerminal();
	}
	
	/**
	 * Bloqueia o terminal evitando o aplicativo finalizar em linha de comando. 
	 * Escaneia o teclado a fim de capturar palavras chave de ajuda ou saida.
	 */
	private static void lockTerminal() {
		String exit = Utility.concat("[JERS] Press 'ctrl + c' to force exit. ",
				"Type 'quit' or 'exit' to finalize. ",
				"Type 'info' or 'help' to more instructions of use. ");
		
		System.out.println(exit);
		
		Scanner scanner = new Scanner(System.in);  
		String in;  
		do {
			in = scanner.next();  
			if("info".equalsIgnoreCase(in)) {
				info(System.out);
			} else if ("help".equalsIgnoreCase(in)) {
				help(System.out);
			}
		} while (!in.equalsIgnoreCase("exit") && !in.equalsIgnoreCase("quit"));
		
	}
	
}
